﻿// Sequin.WebLogs.Sessions.SessionBuilder
//
// (c) 2011 Arthur Pitman
//
// Part of the Sequin Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License (Version 2.1) as published by the Free Software Foundation.
// 
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License (Version 2.1) along with this library; if not, write to the 
// Free Software Foundation, Inc., 
// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
// or see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sequin.WebLogs.IO;

namespace Sequin.WebLogs.Sessions
{
	/// <summary>
	/// Reads <see cref="Session"/> objects from a <see cref="ILogReader"/>
	/// </summary>
	public class SessionReader : ISessionReader
	{
		/// <summary>
		/// The next id to be allocated to a <see cref="Session"/>
		/// </summary>
		protected int nextSessionId = 0;

		/// <summary>
		/// The associated <see cref="ILogReader"/>
		/// </summary>
		protected ILogReader reader;

		/// <summary>
		/// Amount of time that must elapse before a new session begins
		/// </summary>
		protected TimeSpan timeout;

		/// <summary>
		/// Amount of time before a session is purged after timing out
		/// </summary> 
		protected TimeSpan purgeTimeout;

		/// <summary>
		/// Maximum size of the session cache before a purge check is triggered
		/// </summary>
		protected int sessionCacheSizeThreshold;

		/// <summary>
		/// Queue of completed <see cref="Session"/> objects ready for retrieval
		/// </summary>
		protected Queue<Session> doneSessions = new Queue<Session>();

		/// <summary>
		/// Dictionary of <see cref="Session"/> objects in the process of being constructed
		/// </summary>
		protected Dictionary<UserIdentity, Session> sessionCache = new Dictionary<UserIdentity, Session>();

		/// <summary>
		/// Initializes a new <see cref="SessionReader"/> instance with a default timeout value of 30 minutes and purge timeout of 2 hours
		/// </summary>
		/// <param name="reader">The ILogReader to read LogEntry instances from</param>
		public SessionReader(ILogReader reader)
			: this(reader, new TimeSpan(0, 30, 0), new TimeSpan(2, 0, 0), 1000)
		{            
		}

		/// <summary>
		/// Initializes a new <see cref="SessionReader"/> instance with the specified timeout value and purge timeout 
		/// </summary>
		/// <param name="reader">The <see cref="ILogReader"/> to read <see cref="LogEntry"/> instances from</param>
		/// <param name="timeout">Timeout value between sessions</param>
		/// <param name="purgeTimeout">Timeout before a session is purged</param>
		/// <param name="sessionCacheSizeThreshold">Maximum size of the session cache before a purge check is triggered</param>
		public SessionReader(ILogReader reader, TimeSpan timeout, TimeSpan purgeTimeout, int sessionCacheSizeThreshold)
		{
			this.reader = reader;
			this.timeout = timeout;
			this.purgeTimeout = purgeTimeout;
			this.sessionCacheSizeThreshold = sessionCacheSizeThreshold;
		}

		/// <summary>
		/// Reads a completed <see cref="Session"/>
		/// </summary>
		/// <returns>The <see cref="Session"/> or null if none is available</returns>
		public Session ReadSession()
		{
			// keep working to create sessions until:
			// a. a session is complete
			// b. source data is exhausted, in which case the session cache is flushed
			while (doneSessions.Count == 0)
			{
				var entry = reader.ReadLogEntry();
				if (entry == null)
				{
					// flush remaining sessions
					foreach (var session in sessionCache.Values)
						doneSessions.Enqueue(session);
					sessionCache.Clear();
					if (doneSessions.Count == 0)
						return null;
					break;
				}                    

				// update / add
				Session existingSession = null;
				if (sessionCache.TryGetValue(entry.Identity, out existingSession))
				{
					if ((entry.TimeStamp - existingSession.LastEntry.TimeStamp) < timeout)
					{
						existingSession.AddEntry(entry);
					}
					else
					{
						doneSessions.Enqueue(existingSession);
						sessionCache[entry.Identity] = new Session(GetNextSessionId(), entry);
					}
				}
				else
				{
					sessionCache.Add(entry.Identity, new Session(GetNextSessionId(), entry));
				}

				// purge really expired sessions - we do this for performance
				if (sessionCache.Count > sessionCacheSizeThreshold)
				{
					var ngSessionCache = new Dictionary<UserIdentity, Session>();
					foreach (var session in sessionCache.Values)
					{
						if ((entry.TimeStamp - session.LastEntry.TimeStamp) < purgeTimeout)
							ngSessionCache.Add(session.LastEntry.Identity, session);
						else
							doneSessions.Enqueue(session);
					}
					sessionCache = ngSessionCache;
				}
			}
			
			return doneSessions.Dequeue();
		}

		/// <summary>
		/// Gets the next session id
		/// </summary>
		/// <returns>The next session id</returns>
		protected int GetNextSessionId()
		{
			var id = nextSessionId;
			nextSessionId++;
			return id;
		}
	}
}
